package com.edvard.demo;

import android.Manifest;
import android.app.Fragment;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Trace;
import android.util.Size;
import android.view.Surface;
import android.view.WindowManager;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.edvard.demo.env.ImageUtils;
import com.edvard.demo.env.Logger;

import java.nio.ByteBuffer;

/**
 * description :
 * author : edvardzeng
 * email : edvardzeng@tencent.com
 * date : 2020/6/26 16:06
 */
public abstract class CameraBaseActivity extends AppCompatActivity implements ImageReader.OnImageAvailableListener {
	private static final Logger LOGGER = new Logger();

	private static final int PERMISSIONS_REQUEST = 1;

	private static final String PERMISSION_CAMERA = Manifest.permission.CAMERA;
	protected int previewWidth = 0;
	protected int previewHeight = 0;
	private boolean debug = false;
	private Handler handler;
	private HandlerThread handlerThread;
	private boolean useCamera2API;
	private boolean isProcessingFrame = false;
	private byte[][] yuvBytes = new byte[3][];
	private int[] rgbBytes = null;
	// TODO: 测试用，获取 yuv 的代码不优雅，后续优化
//	private byte[] yuvBytes2 = null;

	private int yRowStride;
	private Runnable postInferenceCallback;
	private Runnable imageConverter;

	@Override
	protected void onCreate(@Nullable Bundle savedInstanceState) {
		// TODO: onCreate, protected methods
		LOGGER.d("onCreate " + this);
		super.onCreate(savedInstanceState);

		getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
		setContentView(R.layout.activity_main);

		if (hasPermission()) {
			setFragment();
		} else {
			requestPermission();
		}
	}

	/**
	 * Request permission
	 * Choose Camera
	 * Setup Fragment
	 */

	protected void setFragment() {

		String cameraId = chooseCamera();

		Fragment fragment = null;
		if (useCamera2API) {
			CameraConnectionFragment camera2Fragment =
					CameraConnectionFragment.newInstance(
							new CameraConnectionFragment.ConnectionCallback() {
								@Override
								public void onPreviewSizeChosen(final Size size, final int rotation) {
									previewHeight = size.getHeight();
									previewWidth = size.getWidth();
									CameraBaseActivity.this.onPreviewSizeChosen(size, rotation);
								}
							},
							this,
							R.layout.camera_connection_fragment_tracking,
							getDesiredPreviewFrameSize());

			camera2Fragment.setCamera(cameraId);
			fragment = camera2Fragment;
		}

		getFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();
	}

	private boolean isHardwareLevelSupported(
			CameraCharacteristics characteristics, int requiredLevel) {
		int deviceLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
		if (deviceLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
			return requiredLevel == deviceLevel;
		}
		// deviceLevel is not LEGACY, can use numerical sort
		return requiredLevel <= deviceLevel;
	}


	private String chooseCamera() {
		final CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
		try {
			for (final String cameraId : manager.getCameraIdList()) {
				final CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);

				final Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
				// TODO: We need front facing camera here
				if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
					continue;
				}

				final StreamConfigurationMap map =
						characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

				if (map == null) {
					continue;
				}

				// Fallback to camera1 API for internal cameras that don't have full support.
				// This should help with legacy situations where using the camera2 API causes
				// distorted or otherwise broken previews.
//				useCamera2API =
//						(facing == CameraCharacteristics.LENS_FACING_EXTERNAL)
//								|| isHardwareLevelSupported(
//								characteristics, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL);

				// TODO: 这里暂时先默认都使用 camera2 吧，因为现在大部分手机基本上都支持
				useCamera2API = true;
				return cameraId;
			}
		} catch (CameraAccessException e) {
			LOGGER.e(e, "Not allowed to access camera");
		}

		return null;
	}


	@Override
	public void onRequestPermissionsResult(
			final int requestCode, final String[] permissions, final int[] grantResults) {
		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
		if (requestCode == PERMISSIONS_REQUEST) {
			if (allPermissionsGranted(grantResults)) {
				setFragment();
			} else {
				requestPermission();
			}
		}
	}

	private static boolean allPermissionsGranted(final int[] grantResults) {
		for (int result : grantResults) {
			if (result != PackageManager.PERMISSION_GRANTED) {
				return false;
			}
		}
		return true;
	}


	private boolean hasPermission() {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			return checkSelfPermission(PERMISSION_CAMERA) == PackageManager.PERMISSION_GRANTED;
		} else {
			return true;
		}
	}

	private void requestPermission() {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			if (shouldShowRequestPermissionRationale(PERMISSION_CAMERA)) {
				Toast.makeText(
						CameraBaseActivity.this,
						"Camera permission is required for this demo",
						Toast.LENGTH_LONG)
						.show();
			}
			requestPermissions(new String[]{PERMISSION_CAMERA}, PERMISSIONS_REQUEST);
		}
	}

	/**
	 * Callback for Camera2 API
	 */

	@Override
	public void onImageAvailable(final ImageReader reader) {
		// We need wait until we have some size from onPreviewSizeChosen
		if (previewWidth == 0 || previewHeight == 0) {
			return;
		}
		if (rgbBytes == null) {
			rgbBytes = new int[previewWidth * previewHeight];
		}

		try {
			final Image image = reader.acquireLatestImage();

			if (image == null) {
				return;
			}

			if (isProcessingFrame) {
				image.close();
				return;
			}
			isProcessingFrame = true;
			Trace.beginSection("imageAvailable");
			final Image.Plane[] planes = image.getPlanes();
			fillBytes(planes, yuvBytes);
			yRowStride = planes[0].getRowStride();
			final int uvRowStride = planes[1].getRowStride();
			final int uvPixelStride = planes[1].getPixelStride();

			imageConverter =
					new Runnable() {
						@Override
						public void run() {
							ImageUtils.convertYUV420ToARGB8888(
									yuvBytes[0],
									yuvBytes[1],
									yuvBytes[2],
									previewWidth,
									previewHeight,
									yRowStride,
									uvRowStride,
									uvPixelStride,
									rgbBytes);

//							yuvBytes2 = ImageUtils.getBytesFromImageAsType(image, ImageUtils.NV21);
						}
					};

			postInferenceCallback =
					new Runnable() {
						@Override
						public void run() {
							image.close();
							isProcessingFrame = false;
						}
					};

			processImage();
		} catch (final Exception e) {
			LOGGER.e(e, "Exception!");
			Trace.endSection();
			return;
		}
		Trace.endSection();
	}

	protected void fillBytes(final Image.Plane[] planes, final byte[][] yuvBytes) {
		// Because of the variable row stride it's not possible to know in
		// advance the actual necessary dimensions of the yuv planes.
		for (int i = 0; i < planes.length; ++i) {
			final ByteBuffer buffer = planes[i].getBuffer();
			if (yuvBytes[i] == null) {
				LOGGER.d("Initializing buffer %d at size %d", i, buffer.capacity());
				yuvBytes[i] = new byte[buffer.capacity()];
			}
			buffer.get(yuvBytes[i]);
		}
	}

	/**
	 * Other helper methods
	 */

	protected int[] getRgbBytes() {
		imageConverter.run();
		return rgbBytes;
	}

//	protected byte[] getYuvBytes2() {
//		imageConverter.run();
//		return yuvBytes2;
//	}


	protected void readyForNextImage() {
		if (postInferenceCallback != null) {
			postInferenceCallback.run();
		}
	}

	protected int getScreenOrientation() {
		switch (getWindowManager().getDefaultDisplay().getRotation()) {
			case Surface.ROTATION_270:
				return 270;
			case Surface.ROTATION_180:
				return 180;
			case Surface.ROTATION_90:
				return 90;
			default:
				return 0;
		}
	}

	protected synchronized void runInBackground(final Runnable r) {
		if (handler != null) {
			handler.post(r);
		}
	}

	/**
	 * lifecycle methods
	 */

	@Override
	public synchronized void onStart() {
		LOGGER.d("onStart " + this);
		super.onStart();
	}

	@Override
	public synchronized void onResume() {
		LOGGER.d("onResume " + this);
		super.onResume();

		handlerThread = new HandlerThread("inference");
		handlerThread.start();
		handler = new Handler(handlerThread.getLooper());
	}

	@Override
	public synchronized void onPause() {
		LOGGER.d("onPause " + this);

		handlerThread.quitSafely();
		try {
			handlerThread.join();
			handlerThread = null;
			handler = null;
		} catch (final InterruptedException e) {
			LOGGER.e(e, "Exception!");
		}

		super.onPause();
	}

	@Override
	public synchronized void onStop() {
		LOGGER.d("onStop " + this);
		super.onStop();
	}

	@Override
	public synchronized void onDestroy() {
		LOGGER.d("onDestroy " + this);
		super.onDestroy();
	}


	/**
	 * Below is abstract methods
	 */
	protected abstract void processImage();

	protected abstract Size getDesiredPreviewFrameSize();

	protected abstract void onPreviewSizeChosen(final Size size, final int rotation);
}
